<?php

/**
 * Класс для работы с базой данных MySQL
 *
 * Реализует функционал соединения с бд, отправкой запросов SELECT, INSERT, UPDATE и более высокоуровневых запросов,
 * таких как вставка/удаление сообщений, комментариев,
 * получение сообщения/комментария по их ID и всех записей из таблиц,
 * обновление сообщения/комментария
 * 
 * @author nk11k
 */
class MySQLDB {
    /** #@+
     * @var String
     */
    
    private $HOSTNAME;
    private $USERNAME;
    private $PASSWORD;
    private $DBNAME;

    /**
     * Имя таблицы сообщений в базе данных
     */
    private $TBLMESSAGES;
    /**
     * Имя таблицы комментариев в базе данных
     */
    private $TBLCOMMENTS;

    /** #@- */

    /**
     * Константа для вызова отладочного кода
     */
    const DBG = TRUE;

    /**
     * Дескриптор соединения с базой данных (ресурс)
     *
     * @access private
     * $var resource
     */
    private $link;

    /**
     * Конструктор класса
     *
     * Устанавливает соединение с базой данных MySQL, выбирает базу и устанавливает кодировку UTF8
     */
    function __construct() {
        if (self::DBG) echo '<br> construct MySQLDB';
        global $cfg;        
        $this->HOSTNAME = $cfg->hostname;
        $this->USERNAME = $cfg->username;
        $this->PASSWORD = $cfg->password;
        $this->DBNAME = $cfg->dbname;
        $this->TBLMESSAGES = $cfg->msgtable;
        $this->TBLCOMMENTS = $cfg->cmttable;

        if (!$link = mysql_connect(
                $this->HOSTNAME, 
                $this->USERNAME, 
                $this->PASSWORD)) {
            echo '<br>Could not connect: ' . mysql_error() . "\n";
        } else {
            if (!mysql_select_db($this->DBNAME)) {
                echo '<br>Cannot select DB' . mysql_error() . "\n";
            }
            else
                mysql_set_charset('UTF8');
        }
    }

    /**
     * Деструктор класса
     *
     * Закрывает соединение с базой данных, если оно было установлено
     */
    function __destruct() {
        if ($this->link)
            mysql_close($link);
    }

    /**
     * Метод для получения всех сообщений из таблицы
     *
     * @access public
     * @return array двумерный массив строк, полученных в результате запроса, или FALSE, если запрос пуст/не удался
     */
    public function getAllMessages() {
        $rows = array();
        $sqlresult = $this->selectRowsFromTable(
                $this->TBLMESSAGES,
                '*',
                '', 
                'datetime DESC');
        
        while ($rows[] = mysql_fetch_array($sqlresult));
        array_pop($rows);
        return $rows;
    }

    /**
     * Метод для получения сообщения из таблицы по его ID
     *
     * @access public
     * @param String $messageid номер сообщения ленты, которое следует запросить из базы
     * @return array строки, полученные в результате запроса, или FALSE, если запрос пуст/не удался
     */
    public function getMessageByID($messageid) {
        if (!is_numeric($messageid)) {
            if (self::DBG) {
                echo ('<br>getMessageByID: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
            }
            return FALSE;
        }
        $sqlresult = $this->selectRowsFromTable(
                $this->TBLMESSAGES,
                '*', 
                'messageid = ' . $messageid);
        
        $row = mysql_fetch_array($sqlresult);
        return $row;
    }

    /**
     * Метод для получения всех комментариев из таблицы
     *
     * @access public
     * @param Integer $messageid номер сообщения ленты, для которого следует получить комментарии
     * @return array двумерный массив строк, полученных в результате запроса, или FALSE, если запрос пуст/не удался
     */
    public function getAllCommentsToMessage($messageid) {
        if (!is_numeric($messageid)) {
            if (self::DBG) {
                echo ('<br>getAllCommentsToMessage: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
            }
            return FALSE;
        }
        $rows = array();
        $sqlresult = $this->selectRowsFromTable(
                $this->TBLCOMMENTS,
                '*',
                'messageid = ' . $messageid,
                'datetime ASC');
        
        while ($rows[] = mysql_fetch_array($sqlresult));
        array_pop($rows);
        return $rows;
    }

    /**
     * Метод для получения комментария из таблицы по его ID
     *
     * @access public
     * @param Integer $messageid номер сообщения ленты, для которого следует получить комментарий
     * @param Integer $cmtid номер комментария
     * @return array строки, полученные в результате запроса, или FALSE, если запрос пуст/не удался
     */
    public function getCommentaryByID($messageid, $cmtid) {
        if (!is_numeric($messageid) || !is_numeric($cmtid)) {
            if (self::DBG) {
                echo ('<br>getCommentaryByID: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$cmtid is ' . gettype($cmtid));
            }
            return FALSE;
        }
        $sqlresult = $this->selectRowsFromTable(
                $this->TBLCOMMENTS,
                '*',
                'messageid = ' . $messageid . ' AND ' . 'commentid = ' . $cmtid,
                '');        
        
        $row = mysql_fetch_array($sqlresult);
        return $row;
    }
    /**
     * Метод для добавления сообщения в ленту
     *
     * @access public
     * @param String $text текст сообщения
     * @param String $author имя автора сообщения
     * @return Boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    public function addMessage($text, $author) {
        if (!is_string($text) || !is_string($author)) {
             if (self::DBG) {
                echo ('<br>addMessage: wrong argument type');
                echo ('<br>$text is ' . gettype($text));
                echo ('<br>$author is ' . gettype($author));
             }
            return FALSE;                        
        }
        $success = $this->insertRowIntoTable(
                $this->TBLMESSAGES,
                'text, author, datetime, modified',
                $this->escapeWrapString($text) . ", " .
                $this->escapeWrapString($author) .
                ", NOW(), NOW()");
        
        if ($success && self::DBG)
            echo "<br>Message successfully added.\n";
        else if (self::DBG)
            echo "<br>Error: message wasn't added.\n";
        return $success;
    }

    /**
     * Метод для добавления комментария в ленту
     *
     * @access public
     * @param Integer $messageid номер сообщения ленты, для которого следует добавить комментарий
     * @param String $text текст сообщения
     * @param String $author имя автора сообщения
     * @return Boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    public function addCommentary($messageid, $text, $author) {
        if (!is_numeric($messageid) || 
            !is_string($text)       || 
            !is_string($author)) {
             if (self::DBG) {
                echo ('<br>addCommentary: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$text is ' . gettype($text));
                echo ('<br>$author is ' . gettype($author));
             }
            return FALSE;            
        }
        
        $success = $this->insertRowIntoTable(
                $this->TBLCOMMENTS,
                'messageid, text, author, datetime',
                "$messageid, " .
                $this->escapeWrapString($text) . ", " .
                $this->escapeWrapString($author) .
                ", NOW()");
        
        if ($success && self::DBG)
            echo "<br>Commentary successfully added.\n";
        else if (self::DBG)
            echo "<br>Error: commentary wasn't added.\n";
        return $success;
    }

    /**
     * Метод для обновления сообщения в ленте
     *
     * @access public
     * @param Integer $messageid номер сообщения ленты, который следует изменить
     * @param String $text текст сообщения
     * @param String $author имя автора сообщения
     * @return Boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    public function updateMessage($messageid, $text, $author) {
        if (!is_numeric($messageid) || 
            !is_string($text)       || 
            !is_string($author)) {
             if (self::DBG) {
                echo ('<br>updateMessage: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$text is ' . gettype($text));
                echo ('<br>$author is ' . gettype($author));
             }
            return FALSE;
            }            
        $success = $this->updateRowsFromTableByClause(
                $this->TBLMESSAGES,
                "text = " . $this->escapeWrapString($text) .
                ", author = " . $this->escapeWrapString($author) .
                ", modified = NOW()",
                "messageid = $messageid");
        if ($success && self::DBG)
            echo "<br>Message successfully updated.\n";
        else if (self::DBG)
            echo "<br>Error: message wasn't updated.\n";
        return $success;
    }

    /**
     * Метод для обновления комментария в ленте
     *
     * @access public
     * @param Integer $messageid номер сообщения ленты, который содержит комментарий
     * @param Integer $cmtid номер комментария, который следует изменить
     * @param String $text текст сообщения
     * @param String $author имя автора сообщения
     * @return Boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    public function updateCommentary($messageid, $cmtid, $text, $author) {
        if (!is_numeric($messageid) || 
            !is_numeric($cmtid)     || 
            !is_string($text)       || 
            !is_string($author)) {
             if (self::DBG) {
                echo ('<br>updateCommentary: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$cmtid is ' . gettype($cmtid));
                echo ('<br>$text is ' . gettype($text));
                echo ('<br>$author is ' . gettype($author));
            }
            return FALSE;
           }        
        $success = $this->updateRowsFromTableByClause(
                $this->TBLCOMMENTS,
                "text = " . $this->escapeWrapString($text) .
                ", author = " . $this->escapeWrapString($author),
                "messageid = $messageid AND commentid = $cmtid");
        if ($success && self::DBG)
            echo "<br>Commentary successfully updated.\n";
        else if (self::DBG)
            echo "<br>Error: commentary wasn't updated.\n";
        return $success;
    }

    /**
     * Метод для удаления сообщения из ленты
     * 
     * Выполняет SQL-запрос на удаления сообщения по его $messageid, при этом
     * комментарии корректно удаляются, поскольку таблица имеет параметр ON DELETE CASCADE
     * 
     * @access public
     * @param integer $messageid номер сообщения
     * @return boolean в случае ошибки возвращает false и наоборот
     */
    public function deleteMessage($messageid) {
        if (!is_numeric($messageid)) {
            if (self::DBG) {
                echo ('<br>deleteMessage: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
            }
            return FALSE;
        }
        $success = $this->deleteRowsFromTableByClause(
                $this->TBLMESSAGES,
                "messageid = $messageid");
        
        if ($success && self::DBG)
            echo "<br>Message successfully deleted.\n";
        else if (self::DBG)
            echo "<br>Error: message wasn't deleted.\n";
        return $success;
    }
    
    /**
     * Метод для удаления комментария из ленты
     * 
     * Выполняет SQL-запрос на удаления комментария по его $messageid и $commentaryid
     * 
     * @access public
     * @param integer $messageid номер сообщения
     * @param integer $commentaryid номер комментария
     * @return boolean в случае ошибки возвращает false и наоборот
     */
    public function deleteCommentary($messageid, $commentaryid) {
        if (!is_numeric($messageid) || !is_numeric($commentaryid)) {
            if (self::DBG) {
                echo ('<br>deleteCommentary: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$commentaryid is ' . gettype($commentaryid));
            }
            return FALSE;
        }
        $success = $this->deleteRowsFromTableByClause(
                $this->TBLCOMMENTS,
                "messageid = $messageid AND commentid = $commentaryid");
        
        if ($success && self::DBG)
            echo "<br>Commentary successfully deleted.\n";
        else if (self::DBG)
            echo "<br>Error: commentary wasn't deleted.";
        return $success;
    }

    /**
     * Метод для выбора строк из таблицы
     *
     * @access private
     * @param String $tablename Имя таблицы, из которой происходит выборка
     * @param String $columns содержит список столбцов через запятую, по умолчанию выводятся все столбцы ('*')
     * @param String $clause содержит список всех условий, по умолчанию пустой (выводятся все строки)
     * @param String $orderby содержит список условий упорядочивания результатов запроса, по умолчанию пустой
     * @return Resource в случае успешного завершения метода, иначе - FALSE
     */
    private function selectRowsFromTable($tablename, $columns = '*', $clause = '', $orderby = '') {
        if (!is_string($tablename) || 
            !is_string($columns)   || 
            !is_string($clause)    || 
            !is_string($orderby)) {
             if (self::DBG) {
                echo ('<br>selectRowsFromTable: wrong argument type');
                echo ('<br>$tablename is ' . gettype($tablename));
                echo ('<br>$columns is ' . gettype($columns));
                echo ('<br>$clause is ' . gettype($clause));
                echo ('<br>$orderby is ' . gettype($orderby));
            }
            return FALSE;
           }
        
        if (!empty($clause))
            $whereclause = 'WHERE ' . ($clause);
        else
            $whereclause = '';
        if (!empty($orderby))
            $orderclause = 'ORDER BY ' . ($orderby);
        else
            $orderclause = '';
        $query = sprintf(
                "SELECT %s FROM %s %s %s", 
                ($columns),
                ($tablename),
                $whereclause, $orderclause);

        // Выполняем запрос
        $result = mysql_query($query);

        // Проверяем результат
        // Показываем ошибку MySQL и ошибочный запрос. Полезно для отладки
        if (!$result && self::DBG) {
            $message = 'Invalid query: ' . mysql_error() . "\n";
            $message .= 'Whole query: ' . $query;
            echo "<br>$message";
        }
        return $result;
    }

    /**
     * Метод для обновления столбцов строк в таблице по условию
     *
     * @access private
     * @param String $tablename Имя таблицы, из которой происходит выборка
     * @param String $setcolumns содержит список 'столбец=значение' через запятую
     * @param String $clause содержит список всех условий, по умолчанию пустое (обновляются все строки)
     * @return Boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    private function updateRowsFromTableByClause($tablename, $setcolumns = '', $clause = '') {
        if (!is_string($tablename)  ||
            !is_string($setcolumns) || 
            !is_string($clause)) return FALSE;
        
        if (empty($setcolumns) && self::DBG) {
            echo '<br>Update query failed: @param setcolumns missing' . "\n";
            return FALSE;
        }
        if (!empty($clause))
            $whereclause = 'WHERE ' . $clause;
        else
            $whereclause = '';
        $query = sprintf(
                "UPDATE %s SET %s %s",
                ($tablename),
                ($setcolumns),
                ($whereclause));

        // Выполняем запрос
        $result = mysql_query($query);

        // Проверяем результат
        // Показываем ошибку MySQL и ошибочный запрос. Полезно для отладки
        if (!$result && self::DBG) {
            $message = 'Invalid query: ' . mysql_error() . "\n";
            $message .= 'Whole query: ' . $query;
            echo "<br>$message";
        }
        return $result;
    }

    /**
     * Метод для удаления строк в таблице по условию
     *
     * @access private
     * @param String $tablename имя таблицы, из которой происходит выборка
     * @param String $clause содержит список всех условий, по умолчанию пустое (удаляются все строки)
     * @return Boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    private function deleteRowsFromTableByClause($tablename, $clause = '') {
        if (!is_string($tablename) || !is_string($clause)) return FALSE;
        $whereclause = '';
        if (!empty($clause))
            $whereclause = 'WHERE ' . $clause;
        else
            $whereclause = '';
        $query = sprintf(
                "DELETE FROM %s %s",
                $tablename,
                $whereclause);

        // Выполняем запрос
        $result = mysql_query($query);

        // Проверяем результат
        // Показываем ошибку MySQL и ошибочный запрос. Полезно для отладки
        if (!$result && self::DBG) {
            $message = 'Invalid query: ' . mysql_error() . "\n";
            $message .= 'Whole query: ' . $query;
            echo "<br>$message";
        }
        return $result;
    }

    /**
     * Метод для вставки строк в таблицу
     *
     * В случае, если аргументы $columnnames и $columnvalues не определены, то происходит вставка строки со столбцами, инициализированными по-умолчанию
     *
     * @access private
     * @param String $tablename имя таблицы, куда происходит вставка данных
     * @param String $columnnames содержит список названий столбцов через запятую, по умолчанию пусто
     * @param String $columnvalues содержит список значений столбцов (имя таблиц заключать в обратные апострофы '`', названия столбцов - в "'") через запятую, по умолчанию пусто
     * @return Boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    private function insertRowIntoTable($tablename, $columnnames = '', $columnvalues = '') {
        if (!is_string($tablename)   || 
            !is_string($columnnames) || 
            !is_string($columnvalues)) return FALSE;
        
        if (empty($tablename) && self::DBG) {
            echo "<br>Invalid call: @param tablename is empty.\n";
            return FALSE;
        }
        $query = sprintf(
                "INSERT INTO `%s` (%s) VALUES(%s)",
                $tablename,
                $columnnames,
                $columnvalues);

        // Выполняем запрос
        $result = mysql_query($query);

        // Проверяем результат
        // Показываем ошибку MySQL и ошибочный запрос. Полезно для отладки
        if (!$result && self::DBG) {
            $message = 'Invalid query: ' . mysql_error() . "\n";
            $message .= 'Whole query: ' . $query;
            echo "<br>$message";
        }
        return $result;
    }
    
    private function escapeWrapString($unescapedstring) {
        if (is_string($unescapedstring))
        return "'" . mysql_real_escape_string($unescapedstring) . "'";
        return "''";        
    }

}
